#' Match a correlation function based on a name
#'
#' Translate a function name into an R function.
#'
#' @param cor_func string of the name of the correlation to be use
#'
#' @return A function corresponding to the correlation required
#'
#' @importFrom WGCNA bicor cor

.cor_func_match <- function(cor_func = c("pearson", "spearman", "bicor")){
  # Checks
  if (is.null(cor_func))
    stop("cor_func must be a character vector representing one of the",
         " supported correlation functions.")
  cor_func <- match.arg(cor_func)

  # Function assignation
  if (cor_func == "pearson") {
    cor_func <- function(x) WGCNA::cor(x, method = "pearson", use = "e")
  } else if (cor_func == "spearman") {
    cor_func <- function(x) stats::cor(x, method = "spearman", use = "e")
  } else if (cor_func == "bicor") {
    cor_func <- function(x) WGCNA::bicor(x)
  } else {
    stop("Should never be triggered")
  }
}


# Removing errors about dplyr data-variables
utils::globalVariables(c("Power", "SFT.R.sq"))

#' Calculating best fit of a power low on correlation matrix computed on
#' expression data
#'
#' Adjust a correlation matrix depending of the type of network, then try to
#' parameter a power law for best fit
#'
#' @param cor_mat matrix or data.frame of genes correlation.
#' @param fit_cut_off float, cut off by which R^2 (coefficient of
#' determination) will be thresholded. Must be in ]0;1[.
#' @param network_type string giving type of network to be used. Either
#' "unsigned", "signed", "signed hybrid". See details.
#' @param block_size integer giving size of blocks by which operations can be
#' proceed. Helping if working with low capacity computers. If null, will be
#' estimated.
#' @param ... any other parameter compatible with
#' \code{\link{pickSoftThreshold.fromSimilarity}}
#'
#' @details
#' network_type indicate which transformation will be applied on the
#' correlation matrix to return the similarity score.
#' \describe{
#'   \item{signed}{will modify the range [-1;1] to [0.5;1.5] (because of log10
#'   beeing used for scale free index computation)}
#'   \item{unsigned}{will return absolute value (moving from [-1;1] to [0;1])}
#'   \item{signed hybrid}{will replace all negative values by 0 (moving from
#'   [-1;1] to [0;1])}
#' }
#'
#' @return A list containing power of the law for best fit, fit table, and
#' metadata about the arguments used.
#' @examples
#' get_fit.cor(cor_mat = cor(kuehne_expr[, seq_len(100)]))
#'
#' @importFrom WGCNA pickSoftThreshold.fromSimilarity
#' @importFrom magrittr %>%
#' @importFrom dplyr select top_n
#'
#' @export

get_fit.cor <- function(cor_mat, fit_cut_off = 0.90, network_type =
                          c("unsigned", "signed", "signed hybrid"),
                        block_size = NULL, ...){
  # Checking args
  if (!is.matrix(cor_mat)) stop("cor_mat should be a matrix")
  if (any(is.na(cor_mat)))
    warning("cor_mat should not contain any missing value. It may be due to",
            " non variating probe/transcript when you computed your",
            " correlation matrix.")
  if ((any(cor_mat > 1) | any(cor_mat < -1)) & !any(is.na(cor_mat)))
    stop("cor_mat should be filled with value in the [-1,1] range")
  if (nrow(cor_mat) != ncol(cor_mat))
    stop("cor_mat should be a squared matrix")
  if (length(fit_cut_off) != 1 | !is.numeric(fit_cut_off))
    stop("alpha_expr should be a single number")
  if (fit_cut_off < 0 | fit_cut_off > 1) 
    stop("alpha_expr should be a number between 0 and 1")
  network_type <- match.arg(network_type)
  if (!is.null(block_size)) {
    if (block_size < 2 | block_size %% 1 != 0)
      stop("If not NULL, block_size must be a whole number > 1")}

  # Calculating similarity
  similarity <- matrix()
  if (network_type == "unsigned") {
    similarity <- abs(cor_mat)
  } else if (network_type == "signed") {
    similarity <- (1 + cor_mat) / 2
  } else if (network_type == "signed hybrid") {
    cor_mat[cor_mat < 0] <- 0
    similarity <- cor_mat
  }

  # Getting fit
  sft_fit <- quiet(
    WGCNA::pickSoftThreshold.fromSimilarity(similarity = similarity,
                                            RsquaredCut = fit_cut_off,
                                            blockSize = block_size, ...))
  fit_above_cut_off <- TRUE
  if (is.na(sft_fit$powerEstimate)) { # If no fit, taking maximum fitting power
    warning("No fitting power could be found for provided fit_cut_off. Taking",
            " power for maximum fit. See FAQ for known causes.")
    sft_fit$powerEstimate <- sft_fit$fitIndice %>%
      dplyr::top_n(1, SFT.R.sq) %>% dplyr::select(Power) %>% as.numeric()
    fit_above_cut_off <- FALSE
  }

  # Final list with all infos
  fit <- list(
    power_value = sft_fit$powerEstimate,
    fit_table = sft_fit$fitIndice,
    fit_above_cut_off = fit_above_cut_off,
    metadata = list(
      network_type = network_type
    )
  )

  return(fit)
}


#' Calculating best fit of a power low on expression data
#'
#' Computes correlation matrix of the gene expression data, adjust it depending
#' of the type of network, then try to parameter a power law for best fit
#'
#' @param data_expr matrix or data.frame or SummarizedExperiment, expression
#' data with genes as column and samples as row.
#' @param fit_cut_off float, cut off by which R^2 (coefficient of
#' determination) will be thresholded. Must be in ]0;1[.
#' @param cor_func string specifying correlation function to be used. Must be
#' one of "pearson", "spearman", "bicor", "other". If "other", your_func must
#' be provided
#' @param your_func function returning correlation values. Final values must be
#' in [-1;1]
#' @param network_type string giving type of network to be used. Either
#' "unsigned", "signed", "signed hybrid". See details.
#' @param block_size integer giving size of blocks by which operations can be
#' proceed. Helping if working with low capacity computers. If null, will be
#' estimated.
#' @param ... any other parameter compatible with
#' \code{\link{pickSoftThreshold.fromSimilarity}}
#'
#' @details
#' network_type indicate which transformation will be applied on the
#' correlation matrix to return the similarity score.
#' \describe{
#'   \item{signed}{will modify the range [-1;1] to [0.5;1.5] (because of log10
#'   beeing used for scale free index computation)}
#'   \item{unsigned}{will return absolute value (moving from [-1;1] to [0;1])}
#'   \item{signed hybrid}{will replace all negative values by 0 (moving from
#'   [-1;1] to [0;1])}
#' }
#'
#' @return A list containing power of the law for best fit, fit table, and
#' metadata about the arguments used.
#'
#' @importFrom magrittr %>%
#' @importFrom methods is
#'
#' @examples
#' get_fit.expr(kuehne_expr[, seq_len(100)])
#'
#' @export

get_fit.expr <- function(data_expr, fit_cut_off = 0.90,
                         cor_func = c("pearson", "spearman", "bicor", "other"),
                         your_func = NULL, network_type =
                           c("unsigned", "signed", "signed hybrid"),
                         block_size = NULL, ...){
  # Checking args
  if (methods::is(data_expr, "SummarizedExperiment")) {
    data_expr <- t(SummarizedExperiment::assay(data_expr))
  } else .check_data_expr(data_expr)
  cor_func <- match.arg(cor_func)
  if (cor_func == "other" & (is.null(your_func) | !is.function(your_func)))
    stop("If you specify other, your_func must be a function.")

  # Cor selection
  if (cor_func == "other") {
    cor_to_use <- your_func
  } else {
    cor_to_use <- .cor_func_match(cor_func)
  }

  # Calculating correlation matrix
  cor_mat <- cor_to_use(data_expr %>% as.matrix)

  # If personnal function, checking it returns a matrix with values in [-1;1]
  if (cor_func == "other"){
    if (min(cor_mat) < -1 | max(cor_mat) > 1)
      stop("your_func must be a function which returns values in [-1;1]")
  }

  # Getting fit
  fit <- get_fit.cor(cor_mat = cor_mat, fit_cut_off = fit_cut_off,
                     network_type = network_type, block_size = block_size, ...)

  # Final list with all infos
  fit$metadata$cor_func = ifelse(
    cor_func == "other",
    paste("other:", deparse(substitute(your_func))),
    cor_func)

  return(fit)
}


# Removing errors about dplyr data-variables
utils::globalVariables(c("n_samples_min", "n_samples_max"))

#' Network building by co-expression score computation
#'
#' Compute the adjacency matrix, then the TOM to build the network. Than detect
#' the modules by hierarchical clustering and thresholding
#'
#' @param data_expr matrix or data.frame or SummarizedExperiment, expression
#' data with genes as column and samples as row.
#' @param fit_cut_off float, cut off by which R^2 (coefficient of
#' determination) will be thresholded. Must be in ]0;1[.
#' @param cor_func string, name of the correlation function to be used. Must be
#' one of "pearson", "spearman", "bicor", "other". If "other", your_func must
#' be provided
#' @param your_func function returning correlation values. Final values must be
#' in [-1;1]
#' @param keep_matrices string, matrices to keep in final object. Can be one of
#' "none", "cor", "adja", "both". It is usefull to keep both if you plant to use
#' \code{\link{compare_conditions}}.
#' @param power_value integer, power to be applied to the adjacency matrix. If
#' NULL, will be estimated by trying different power law fitting.
#' @param block_size integer, size of blocks by which operations can be
#' proceed. Helping if working with low capacity computers. If null, will be
#' estimated.
#' @param stop_if_fit_pb boolean, does not finding a fit above fit_cut_off, or 
#' having a power too low or too high (based on WGCNA FAQ recommended powers)
#' should stop process, or just print a warning and return the highest fitting
#' power.
#' @param pct_power_ic float, confidence interval by which the power fitted
#' should be evaluated for too high or too low a power.
#' @param network_type string, type of network to be used. Either "unsigned",
#' "signed", "signed hybrid". See details.
#' @param tom_type string, type of the topological overlap matrix to be
#' computed. Either "none", "unsigned", "signed", "signed Nowick",
#' "unsigned 2", "signed 2" and "signed Nowick 2". See detail at
#' \code{\link[WGCNA]{TOMsimilarityFromExpr}}.
#' @param n_threads integer, number of threads that can be used to paralellise
#' the computing
#' @param ... any other parameter compatible with
#' \code{\link{adjacency.fromSimilarity}}
#'
#' @return list containing network matrix, metadata of input parameters and
#' power fitting information.
#'
#' @importFrom WGCNA adjacency.fromSimilarity TOMsimilarity
#' @importFrom magrittr %>% set_colnames set_rownames
#' @importFrom SummarizedExperiment assay
#' @importFrom methods is
#'
#' @examples
#' net <- build_net(kuehne_expr[, seq_len(350)], n_threads = 1)
#'
#' @export

build_net <- function(data_expr, fit_cut_off = 0.90, cor_func =
                        c("pearson", "spearman", "bicor", "other"),
                      your_func = NULL, power_value = NULL, block_size = NULL,
                      stop_if_fit_pb = FALSE, pct_power_ic = 0.7,
                      network_type = c("unsigned", "signed", "signed hybrid"),
                      tom_type = c("unsigned", "signed", "signed Nowick",
                                   "unsigned 2", "signed 2", "none"),
                      keep_matrices = c("none", "cor", "adja", "both"),
                      n_threads = NULL, ...)
                      # TODO program the mclapply version
{
  # Checking
  if (methods::is(data_expr, "SummarizedExperiment")) {
    data_expr <- t(SummarizedExperiment::assay(data_expr))
  } else .check_data_expr(data_expr)
  cor_func <- match.arg(cor_func)
  if (cor_func == "other" & (is.null(your_func) | !is.function(your_func)))
    stop("If you specify other, your_func must be a function.")
  if (!is.null(power_value)) {
    if (power_value < 1 | power_value %% 1 != 0)
      stop("If not NULL, power_value must be a whole number >= 1.")}
  if (!is.logical(stop_if_fit_pb))
    stop("stop_if_fit_pb must be a boolean.")
  network_type <- match.arg(network_type)
  tom_type <- match.arg(tom_type)
  keep_matrices <- match.arg(keep_matrices)
  if (!is.null(n_threads)) {
    if (!is.numeric(n_threads) | n_threads < 1 | n_threads %% 1 != 0)
      stop("n_threads must be a whole number >= 1")}

  # WGCNA's multi-threading
  if (n_threads == 1) {
    quiet(WGCNA::disableWGCNAThreads())
  } else quiet(WGCNA::enableWGCNAThreads(n_threads))
  # TODO : change this function for an internal one because WGCNA's function
  # isn't prefixed, causing warnings

  # Correlation selection and correlation matrix computation
  if (cor_func == "other") {
    cor_to_use <- your_func
  } else {
    cor_to_use <- .cor_func_match(cor_func)
  }
  cor_mat <- cor_to_use(data_expr %>% as.matrix)
  if (min(cor_mat < -1) | max(cor_mat) > 1)
    stop("Provided correlation function returned values outside [-1,1].")

  # Getting power
  if (is.null(power_value)) {
    fit <- get_fit.cor(cor_mat = cor_mat, fit_cut_off = fit_cut_off,
                       network_type = network_type, ...)
    if (stop_if_fit_pb & fit$fit_above_cut_off == FALSE)
      stop("No fitting power could be found for provided fit_cut_off.",
           "You should verify your data (or lower fit_cut_off). See FAQ.")
  } else {
    fit <- list(
      power_value = power_value,
      fit_table = "None. Custom power_value provided")
  }
  
  # Checking power (based on question 6 from WGCNA FAQ)
  approx_power <- data.frame(n_samples_min = c(0, 20, 30, 40),
                             n_samples_max = c(20, 30, 40, +Inf),
                             signed = c(18, 16, 14, 16),
                             unsigned = c(9, 8, 7, 6),
                             `signed hybrid` = c(9, 8, 7, 6), 
                             check.names = FALSE)
  n_samples <- nrow(data_expr)
  good_line <- approx_power %>% 
    select(n_samples_min, n_samples_max, !!network_type) %>%
    filter(n_samples >= n_samples_min & n_samples < n_samples_max)
  
  if (as.logical(fit$power_value < good_line[network_type] * pct_power_ic)) {
    msg_fit <- paste0("The power fitted is below the reference (confidence ", 
                      " interval included): ", good_line[network_type], ". ", 
                      "You might want to decrease your fit threshold and/or ",
                      "your data for confounding factors. See FAQ for more ",
                      "details on power fit troubles.")
  } else if (
    as.logical(fit$power_value > good_line[network_type] / pct_power_ic)) {
    msg_fit <- paste0("The power fitted is above the reference (confidence ", 
                      " interval included): ", good_line[network_type], ". ", 
                      "You might want to increase your fit threshold and/or ",
                      "check the scale-free topology fitting through the fit ",
                      "table. See FAQ for more details on power fit troubles.")
  }
  if (exists("msg_fit")) if (stop_if_fit_pb) stop(msg_fit) else warning(msg_fit)

  # Adjacency
  adj = WGCNA::adjacency.fromSimilarity(similarity = cor_mat,
                                        type = network_type,
                                        power = fit$power_value)
  # Outputed matrix class is AsIs. Changing it to avoid later side effects
  class(adj) <- c("matrix", "array")

  # Topological overlap matrix
  if (tom_type != "none") {
    tom <- 1 - quiet(WGCNA::TOMsimilarity(adj, TOMType = tom_type)) %>%
      magrittr::set_colnames(colnames(adj)) %>%
      magrittr::set_rownames(rownames(adj))
  } else { tom <- adj }

  net = list(
    network = tom,
    metadata = list(
      cor_func = cor_func,
      network_type = network_type,
      tom_type = tom_type,
      power = fit$power_value,
      fit_power_table = fit$fit_table
    )
  )

  # Adding matrices to keep to the list
  if (keep_matrices %in% c("cor", "both"))
    net$cor_mat <- cor_mat
  if (keep_matrices %in% c("adja", "both"))
    net$adja_mat <- adj

  return(net)
}


# Removing errors about dplyr data-variables
utils::globalVariables(c("", ""))

#' Modules detection in a network
#'
#' Detect the modules by hierarchical clustering .
#'
#' @param data_expr matrix or data.frame or SummarizedExperiment, expression
#' data with genes as column and samples as row.
#' @param network matrix or data.frame, strengh of gene co-expression (edge
#' values).
#' @param min_module_size integer, lowest number of gene allowed in a module.
#' If none provided, estimated.
#' @param clustering_th float, threshold to be used by the clustering method.
#' For now \code{\link[dynamicTreeCut]{cutreeDynamic}}.
#' @param merge_close_modules boolean, does closest modules (based on
#' eigengene) should be merged together.
#' @param merge_threshold float, eigengenes correlation value over which
#' close modules will be merged. Must be in ]0;1[. See
#' \code{\link[WGCNA]{mergeCloseModules}}
#' @param detailled_result boolean, does pre-merge modules (if applicable)
#' and dendrogram included in output.
#' @param pam_respects_dendro boolean, If TRUE, the Partitioning Around Medoids
#' (PAM) stage will respect the dendrogram in the
#' sense that objects and small clusters will only be assigned to clusters that
#'  belong to the same branch that the objects or
#' small clusters being assigned belong to.
#' @param ... any other parameter compatible with
#' \code{\link[WGCNA]{mergeCloseModules}}
#'
#' @return list containing modules detected, modules_eigengenes, and if asked
#' for, modules pre-merge and dendrograms of genes and merged modules
#'
#' @importFrom WGCNA mergeCloseModules
#' @importFrom dynamicTreeCut cutreeDynamic
#' @importFrom stats setNames as.dist hclust
#' @importFrom methods is
#' @importFrom stringr str_sort
#'
#' @examples
#' df <- kuehne_expr[1:24, 1:350]
#' net <- build_net(df, n_threads = 1)
#' detect_modules(df, net$network)
#'
#' @export

detect_modules <- function(data_expr, network, min_module_size =
                             min(20, ncol(data_expr) / 2),
                           clustering_th = NULL, merge_close_modules = TRUE,
                           merge_threshold = 0.75, detailled_result = TRUE,
                           pam_respects_dendro = FALSE, ...) {
  # Checks
  if (methods::is(data_expr, "SummarizedExperiment")) {
    data_expr <- t(SummarizedExperiment::assay(data_expr))
  } else .check_data_expr(data_expr)
  if (!(is.data.frame(network) | is.matrix(network)))
    stop("network should be a data.frame or a matrix.")
  if (ncol(network) != nrow(network))
    stop("network should be squarred")
  if (is.null(rownames(network)) | !all(colnames(network) %in%
                                        rownames(network)))
    stop("network should have the same genes names as colnames and rownames")
  # TODO : finish checks

  # Order network matrix in the same order as data_expr
  network <- network[colnames(data_expr), colnames(data_expr)]

  # Hierarchical clustering
  gene_tree = stats::hclust(stats::as.dist(network), method = "average")
  # Tree cut
  dynamicMods = quiet(dynamicTreeCut::cutreeDynamic(
    dendro = gene_tree, distM = network, cutHeight = clustering_th,
    pamRespectsDendro = pam_respects_dendro, minClusterSize = min_module_size))
  # TODO manage to divide ellipsis (...) to allow users to add args
  # for cutreeDynamic

  # Re-assign gene names
  dynamicMods <- stats::setNames(dynamicMods, colnames(data_expr))

  if (length(table(dynamicMods)) == 1)
    stop("No modules detected")
  if (length(table(dynamicMods)) == 2)
    warning("Only one module found, plus 'non-classified genes' module")

  # Merging closest modules
  if (merge_close_modules) {
    if (length(table(dynamicMods)) == 1)
      stop("All genes were classified in the same module")
    merge <- quiet(WGCNA::mergeCloseModules(
      data_expr, colors = dynamicMods, cutHeight = 1 - merge_threshold,
      relabel = TRUE, ...))

    # Reordering module eigengenes
    merge$newMEs <- merge$newMEs[, stringr::str_sort(colnames(merge$newMEs),
                                                     numeric = TRUE)]
    merge$newMEs <- merge$newMEs[, stringr::str_sort(colnames(merge$newMEs),
                                                     numeric = TRUE)]

  } else {
    merge <- list(
      colors = dynamicMods,
      newMEs = WGCNA::moduleEigengenes(data_expr, dynamicMods)$eigengenes)
  }

  # Re-formating
  modules_list <- stats::setNames(merge$colors,
                           colnames(data_expr)) %>% split(names(.), .)
  modules_list_premerge <- stats::setNames(dynamicMods,
                                    colnames(data_expr)) %>% split(names(.), .)

  # Return
  if (detailled_result) {
    detection <- list(
      modules = modules_list,
      modules_premerge = modules_list_premerge,
      modules_eigengenes = merge$newMEs,
      dendrogram_genes = gene_tree,
      dendrogram_merged_modules = stats::hclust(
        stats::as.dist(1 - cor(merge$newMEs)),
        method = "average")
    )
  } else {
    detection <- list(
      modules = modules_list,
      modules_eigengenes = merge$newMEs
    )
  }

  return(detection)
}


# Removing errors about dplyr data-variables
utils::globalVariables(c("before", "after"))

#' Modules merge plot
#'
#' Plot a bipartite graph to see in which modules all modules have been merged
#'
#' @param modules_premerge vector, id (whole number or string) of module before
#' merge associated to each gene.
#' @param modules_merged vector, id (whole number or string) of module after
#' merge associated to each gene.
#' @param zoom decimal, value to which the display will be increased/decreased.
#' @param vertex_size integer, size of the vertices.
#' @param vertex_label_color,vertex_color,vertex_frame_color string, name of the
#'  color or hexadecimal code.
#' @param vertex_label_family string, font family name.
#' @param vertex_label_cex decimal, value for font size.
#' @param window_x_min decimal, value for the bottom limit of the window.
#' @param window_x_max decimal, value for the top limit of the window.
#' @param window_y_min decimal, value for the left limit of the window.
#' @param window_y_max decimal, value for the right limit of the window.
#' @param ... additional arguments to be passed to igraph::plot.igraph().
#'
#' @details Both vectors must be in the same gene order before passing them to
#' the function. No check is applied on this.
#'
#' @return The layout of the plot
#'
#' @importFrom igraph graph_from_data_frame V add_layout_ as_bipartite
#' norm_coords
#' @importFrom magrittr %>% set_colnames
#' @importFrom dplyr left_join mutate mutate_if distinct arrange
#' @importFrom utils stack
#' @importFrom stringr str_c str_extract
#' @importFrom tibble column_to_rownames
#'
#' @examples
#' df <- kuehne_expr[1:24, 1:350]
#' net <- build_net(df, n_threads = 1)
#' detection <- detect_modules(df, net$network, detailled_result = TRUE)
#' plot_modules_merge(modules_premerge = detection$modules_premerge,
#'                    modules_merged = detection$modules)
#'
#' @export

plot_modules_merge <- function(modules_premerge, modules_merged,
                               zoom = 1, vertex_size = 6,
                               vertex_label_color = "gray20",
                               vertex_label_family = "Helvetica",
                               vertex_label_cex = 0.8,
                               vertex_color = "lightskyblue",
                               vertex_frame_color = "white",
                               window_x_min = -1, window_x_max = 1,
                               window_y_min = -1, window_y_max = 1, ...) {
  # Checks
  if (!is.list(modules_premerge))
    stop("modules_premerge must be a named list with modules id as names, and",
    " vectors of gene names as content")
  if (!is.list(modules_merged))
    stop("modules_merged must be a named list with modules id as names, and",
    " vectors of gene names as content")
  if (length(modules_premerge) < 2)
    stop("modules_premerge must contain at least 2 modules")
  if (length(modules_merged) < 1 |
      length(modules_merged) > length(modules_premerge))
    stop("modules_premerge must contain at least 1 module and less or equal",
    "number of module")
  if (is.null(names(modules_premerge)))
    stop("modules_premerge must be named with modules id as names ")
  if (is.null(names(modules_merged)))
    stop("modules_merged must be named with modules id as names ")
  if (!all(lapply(modules_merged, is.vector, "character") %>% unlist))
    stop("module_merged values for each module must be a vector of gene names")

  # data.frame indicating which module got merge into another
  g_df <- dplyr::left_join(utils::stack(modules_premerge),
                        utils::stack(modules_merged), by = "values") %>%
      tibble::column_to_rownames("values") %>%
      magrittr::set_colnames(c("before", "after")) %>%
      dplyr::mutate_if(is.factor, as.character) %>%
      dplyr::mutate_if(is.character, as.numeric) %>%
      dplyr::distinct() %>%
      dplyr::arrange(before) %>%
      dplyr::mutate(before = stringr::str_c("b", before, sep = "_")) %>%
      dplyr::mutate(after = stringr::str_c("a", after, sep = "_"))
  g <- igraph::graph_from_data_frame(g_df)

  # Adding property used by bipartite plot
  igraph::V(g)$type <- lapply(
      igraph::V(g) %>% names, # modules premerge
      function(x) ifelse(x %in% (g_df$after %>% unique), # in modules merged
                         TRUE, FALSE)) %>% unlist

  # Bipartite plot
  l <- igraph::layout_as_bipartite(g) %>%
      igraph::norm_coords(l, xmin = window_x_min, xmax = window_x_max,
                          ymin = window_y_min, ymax = window_y_max)

  igraph::plot.igraph(g, layout = l*zoom,
       rescale = ifelse(zoom == 1, TRUE, FALSE),
       vertex.label = igraph::V(g) %>% names() %>% stringr::str_extract("\\d+"),
       vertex.label.color = vertex_label_color,
       vertex.label.family = vertex_label_family,
       vertex.size = vertex_size,
       vertex.label.cex = vertex_label_cex,
       vertex.color = vertex_color,
       vertex.frame.color = vertex_frame_color, ...)

  return(l)
}


# Removing errors about dplyr data-variables
utils::globalVariables(c("module", "gene", "gene_gene", "expression_gene",
                         "expression_eigengene", "cor_sign"))

#' Modules expression profiles
#'
#' Plot expression profiles for all modules with eigengene highlighted
#'
#' @param data_expr matrix or data.frame or SummarizedExperiment, expression
#' data with genes as column and samples as row.
#' @param modules vector, id (whole number or string) of modules associated to
#' each gene.
#' @param eigengenes matrix or data.frame, eigeingenes of the provided modules.
#' If null, new ones will be computed with a PCA.
#' @param alpha_expr numeric, transparency of the expression lines. Must be a
#' value betweem 0 (transparent) and 1 (opaque)
#' @param ... additional parameters to pass to ggplot2::theme
#'
#' @details The sign of the eigengenes from \code{\link{detect_modules}} may 
#' differ from the ones computed by the pca if no eigengenes is provided to 
#' \code{\link{plot_expression_profiles}} and therefore the plot itself. This
#' is du to the sign indeterminancy property from the singular value
#' decomposition.
#'
#' @return A ggplot representing expression profile and eigengene by module
#'
#' @importFrom ggplot2 ggplot aes geom_line facet_grid theme element_text
#' @importFrom tibble rownames_to_column
#' @importFrom tidyr pivot_longer
#' @importFrom magrittr %>%
#' @importFrom dplyr mutate left_join group_by rename select
#' @importFrom stats setNames prcomp
#' @importFrom utils stack
#' @importFrom methods is
#'
#' @examples
#' df <- kuehne_expr[1:24, 1:350]
#' net <- build_net(df, n_threads = 1)
#' detection <- detect_modules(df, net$network, detailled_result = TRUE)
#' plot_expression_profiles(df, detection$modules, detection$modules_eigengenes)
#'
#' @export

plot_expression_profiles <- function(data_expr, modules, eigengenes = NULL,
                                     alpha_expr = 0.3, ...) {
  # Check
  if (methods::is(data_expr, "SummarizedExperiment")) {
    data_expr <- t(SummarizedExperiment::assay(data_expr))
  } else .check_data_expr(data_expr)
  if (is.list(modules)) {
    if (any(!unlist(lapply(modules, is.vector, "character")))) {
      stop("If modules is a list of modules, all elements of the list must be",
      " vectors of gene names") }
    if (is.null(names(modules)))
      warning("No name provided for the list of modules.")
  } else if (!is.vector(modules, "character")) {
    stop("modules must be either a list of modules or a single module",
    " represented by a vector of gene names")
  } else if (!is.list(modules) & length(modules) < 2) {
    warning("modules represent a single modules and only contains one",
    " gene name")
  }
  if (!is.null(eigengenes)) {
    if (!is.data.frame(eigengenes) & !is.matrix(eigengenes))
      stop("eigengenes must be a data.frame or a matrix")
    if (length(modules) != ncol(eigengenes))
      stop("eigengenes must have the same number of columns as the number of ",
           "modules provided")
  }
  if (length(alpha_expr) != 1 | !is.numeric(alpha_expr))
    stop("alpha_expr should be a single number")
  if (alpha_expr < 0 | alpha_expr > 1) 
    stop("alpha_expr should be a number between 0 and 1")

  # Tables preparation for ggplot
  if (is.list(modules)) {
    df <- modules %>%
      utils::stack() %>%
      stats::setNames(c("gene", "module")) %>%
      dplyr::mutate(module = as.character(module))
  } else {
    df <- data.frame(gene = modules,
                     module = "module",
                     stringsAsFactors = FALSE) %>%
      dplyr::mutate(module = as.character(module))
  }

  if (is.null(eigengenes)) {
    eigengenes <- df %>%
      dplyr::left_join(data_expr %>%
                         t %>%
                         as.data.frame %>%
                         tibble::rownames_to_column(var = "gene"),
                       by = "gene") %>%
      split.data.frame(.$module) %>%
      lapply(function(y) y[,c(-1,-2)] %>%
               t %>%
               stats::prcomp(center = TRUE, scale = TRUE) %>%
               .$x %>%
               .[, "PC1"] %>%
               scale) %>%
      as.data.frame(check.names = FALSE) %>%
      tibble::rownames_to_column(var="sample") %>%
      dplyr::mutate(gene = "eigengene") %>%
      tidyr::pivot_longer(c(-gene, -sample),
                          names_to = "module",
                          values_to = "expression")
  } else {
    eigengenes <- eigengenes %>%
      as.data.frame() %>%
      scale() %>%
      as.data.frame(check.names = FALSE) %>%
      tibble::rownames_to_column(var="sample") %>%
      dplyr::mutate(gene = "eigengene") %>%
      tidyr::pivot_longer(c(-gene, -sample),
                          names_to = "module",
                          values_to = "expression") %>%
      mutate(module = stringr::str_extract(module, "\\d+"))
  }
  
  # Formating eigengenes and genes expression to be compatible for ggplot2
  plot_table <- df %>%
    dplyr::left_join(data_expr %>%
                       scale %>%
                       as.data.frame %>%
                       t %>%
                       as.data.frame %>%
                tibble::rownames_to_column(var = "gene"),
                by = "gene") %>%
    tidyr::pivot_longer(c(-gene, -module),
                        names_to = "sample",
                        values_to = "expression") %>%
    dplyr::left_join(eigengenes,
                     c("module", "sample"),
                     suffix = c("_gene", "_eigengene")) %>%
    dplyr::group_by(gene_gene) %>%
    dplyr::mutate(cor_sign = ifelse(
      cor(expression_gene, expression_eigengene) >= 0, "+", "-")) %>%
    dplyr::mutate(expression_eigengene = ifelse(
      cor_sign == "-", -(expression_eigengene), expression_eigengene)) %>%
    dplyr::rename(gene = gene_gene) %>%
    dplyr::rename(expression = expression_gene) %>%
    dplyr::select(gene, module, sample, expression, cor_sign,
                  expression_eigengene)

  # Plotting
  ggplot2::ggplot(plot_table,
                  ggplot2::aes(x=sample, y=expression, group=gene)) +
    ggplot2::geom_line(alpha = alpha_expr) +
    ggplot2::facet_grid(cor_sign ~ module) +
    ggplot2::theme(axis.text.x = ggplot2::element_blank(),
                   axis.ticks.x = ggplot2::element_blank(),
                   legend.position = "none",
                   ...) +
    ggplot2::geom_line(ggplot2::aes(x = sample, y = expression_eigengene),
                       color = "red")
}
